package io.kwdb.sdk.internal;

import io.kwdb.proto.Ingest;
import io.kwdb.sdk.Point;
import io.kwdb.sdk.QueryResult;
import io.kwdb.sdk.Status;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Utility class for mapping between SDK models and protobuf messages. This class handles all
 * conversions between the public API models and the internal protobuf representations.
 */
public final class ProtoMappers {

  // Private constructor to prevent instantiation
  private ProtoMappers() {
    throw new AssertionError("ProtoMappers is a utility class and should not be instantiated");
  }

  /**
   * Convert a single Point to a WriteRequest.
   *
   * @param point The point to convert
   * @return The WriteRequest protobuf message
   */
  public static Ingest.WriteRequest toWriteRequest(Point point) {
    return toWriteRequest(List.of(point));
  }

  /**
   * Convert a list of Points to a WriteRequest.
   *
   * @param points The list of points to convert
   * @return The WriteRequest protobuf message
   */
  public static Ingest.WriteRequest toWriteRequest(List<Point> points) {
    Ingest.WriteRequest.Builder requestBuilder = Ingest.WriteRequest.newBuilder();

    for (Point point : points) {
      requestBuilder.addPoints(toProtoPoint(point));
    }

    return requestBuilder.build();
  }

  /**
   * Convert an SDK Point to a protobuf Point.
   *
   * @param point The SDK point to convert
   * @return The protobuf Point message
   */
  private static Ingest.Point toProtoPoint(Point point) {
    Ingest.Point.Builder pointBuilder =
        Ingest.Point.newBuilder().setMeasurement(point.getMeasurement());

    // Add tags
    for (Map.Entry<String, String> entry : point.getTags().entrySet()) {
      pointBuilder.addTags(
          Ingest.Tag.newBuilder().setKey(entry.getKey()).setValue(entry.getValue()).build());
    }

    // Add fields
    for (Map.Entry<String, Object> entry : point.getFields().entrySet()) {
      pointBuilder.addFields(toProtoField(entry.getKey(), entry.getValue()));
    }

    // Add timestamp if present
    if (point.hasTimestamp()) {
      pointBuilder.setTimestampUnixNs(point.getTimestamp());
    }

    return pointBuilder.build();
  }

  /**
   * Convert a field key-value pair to a protobuf Field. Note: The current proto only supports
   * double (f64) field type.
   *
   * @param key The field key
   * @param value The field value
   * @return The protobuf Field message
   */
  private static Ingest.Field toProtoField(String key, Object value) {
    Ingest.Field.Builder fieldBuilder = Ingest.Field.newBuilder().setKey(key);

    // Convert all types to double
    double doubleValue;
    if (value instanceof Double) {
      doubleValue = (Double) value;
    } else if (value instanceof Float) {
      doubleValue = ((Float) value).doubleValue();
    } else if (value instanceof Long) {
      doubleValue = ((Long) value).doubleValue();
    } else if (value instanceof Integer) {
      doubleValue = ((Integer) value).doubleValue();
    } else if (value instanceof Boolean) {
      doubleValue = ((Boolean) value) ? 1.0 : 0.0;
    } else if (value instanceof String) {
      try {
        doubleValue = Double.parseDouble((String) value);
      } catch (NumberFormatException e) {
        doubleValue = 0.0;
      }
    } else if (value != null) {
      try {
        doubleValue = Double.parseDouble(value.toString());
      } catch (NumberFormatException e) {
        doubleValue = 0.0;
      }
    } else {
      doubleValue = 0.0;
    }

    fieldBuilder.setF64(doubleValue);
    return fieldBuilder.build();
  }

  /**
   * Create a QueryReq from SQL string.
   *
   * @param sql The SQL query string
   * @return The QueryReq protobuf message
   */
  public static Ingest.QueryReq toQueryReq(String sql) {
    return Ingest.QueryReq.newBuilder()
        .setSql(sql)
        .setFetchSize(0) // 0 means fetch all
        .build();
  }

  /**
   * Convert a QueryResp to a QueryResult.
   *
   * @param response The protobuf QueryResp
   * @return The SDK QueryResult
   */
  public static QueryResult fromQueryResp(Ingest.QueryResp response) {
    // Check for error
    if (response.getCode() != 0) {
      return QueryResult.error(
          Status.error(Status.Code.fromValue(response.getCode()), response.getMessage()));
    }

    QueryResult.Builder builder = QueryResult.builder();

    // Parse rows - each QueryRow contains columns and values
    List<Ingest.QueryRow> protoRows = response.getRowsList();
    if (protoRows.isEmpty()) {
      return builder.build(); // Empty result
    }

    // Extract column names from first row
    List<QueryResult.Column> columns = new ArrayList<>();
    Ingest.QueryRow firstRow = protoRows.get(0);
    for (String colName : firstRow.getColumnsList()) {
      columns.add(new QueryResult.Column(colName, "string")); // Type unknown in this format
    }
    builder.columns(columns);

    // Convert all rows
    List<QueryResult.Row> rows = new ArrayList<>();
    for (Ingest.QueryRow protoRow : protoRows) {
      QueryResult.Row row = new QueryResult.Row();

      List<String> colNames = protoRow.getColumnsList();
      List<String> values = protoRow.getValuesList();

      for (int i = 0; i < colNames.size() && i < values.size(); i++) {
        // Try to parse as number, otherwise keep as string
        String valueStr = values.get(i);
        Object value;

        if ("NULL".equals(valueStr)) {
          value = null;
        } else {
          // Try to parse as number
          try {
            if (valueStr.contains(".")) {
              value = Double.parseDouble(valueStr);
            } else {
              value = Long.parseLong(valueStr);
            }
          } catch (NumberFormatException e) {
            value = valueStr;
          }
        }

        row.setValue(colNames.get(i), value);
      }
      rows.add(row);
    }
    builder.rows(rows);

    return builder.build();
  }

  /**
   * Create an error QueryResult from an exception message.
   *
   * @param message The error message
   * @return The QueryResult with error status
   */
  public static QueryResult errorResult(String message) {
    return QueryResult.error(message);
  }
}
